/********************************************
 * Class QLyric
 * Qt Lyric 歌词解析类 类实现
 *
 * QLyric.cpp
 * By sunnysab (sunnysab@yeah.net)
 * 不得用于收费软件，请在软件中标明使用本库及作者
 *
*********************************************/

#include "QLyric.h"

#include <QFile>         // Qt 文件操作, 歌词载入和保存依赖此项
#include <QString>       // Qt 内置字符串操作
#include <QByteArray>    // Qt 字节操作
#include <QTextStream>   // Qt 文本操作类


// LRC_DEBUG 控制了类中一些 qDebug() 语句及某些函数
// 定义位于 QLyric.h 中
#ifdef   LRC_DEBUG
    #include <QtDebug>   // Qt 调试功能类
#endif



/*
 * 名    称: QLyric
 * 属    性: 构造函数
 * 功    能:
 * 参    数:
 */
QLyric::QLyric()
{
    RemoveAll();
}


/*
 * 运    算: +
 * 功    能:
 * 说    明:
 */
QLyric QLyric::operator +(const QLyric  &lrc2)
{
    m_LineList += lrc2.m_LineList;

    Sort();

    return  *this;
}


/*
 * 运    算: +=
 * 功    能:
 * 说    明:
 */
QLyric QLyric::operator +=(const QLyric &lrc2)
{
    m_LineList += lrc2.m_LineList;

    Sort();

    return  *this;
}

QLyric QLyric::operator +=(const LYRICLINE &stLyricLine)
{
    AppendLine(stLyricLine);

    return  *this;
}


/*
 * 名    称: CompareTime
 * 属    性: private
 * 功    能: 返回 stLine1 的时间是否在 stLine2 之前
 * 参    数: const LYRICLINE  &stLine1    要比较的时间1
 *          const LYRICLINE  &stLine2    要比较的时间2
 * 说    明: 本函数提供给 sort() 使用的迭代器，见 QLyric::Sort()
 */
bool QLyric::CompareTime(const LYRICLINE &stLine1, const LYRICLINE &stLine2)
{
    return stLine1.nTime < stLine2.nTime;
}



/*
 * 名    称: GetTimeFromString
 * 属    性: public & static
 * 功    能: 将时间标签转换为以毫秒为单位的数值
 * 参    数: const QString       &strTime     要转换的时间标签
 */
LyricTime QLyric::GetTimeFromString(const QString  &strTime)
{
    LyricTime  nXX = 0;  // 百分之一秒
    LyricTime  nSS = 0;  // 秒数
    LyricTime  nMM = 0;  // 分钟数
    int  nIndex = 0;  // 标志转换的起始位置

#ifdef  LRC_DEBUG
    qDebug() << "strTime: " << strTime << endl;
#endif

    // 处理方式
    // [MM:SS.XX] 标签, 将'['作为MM开始标志,以此类推
    if ((nIndex = strTime.indexOf("[")) != -1)
    {
        // 这里的代码写长了，不过也就这个笨办法了
        // 笔记：QString::toInt遇到非字符会返回0
        nMM = strTime.mid(nIndex + 1, strTime.indexOf(":") - nIndex - 1).toInt();
    }

    if ((nIndex = strTime.indexOf(":")) != -1)
    {
        nSS = strTime.mid(nIndex + 1, strTime.indexOf(".") - nIndex - 1).toInt();
    }

    if ((nIndex = strTime.indexOf(".")) != -1)
    {
        nXX = strTime.mid(nIndex + 1, strTime.indexOf("]") - nIndex - 1).toInt();
    }

    return  nMM * 60 * 100 + nSS * 100 + nXX;
}



/*
 * 名    称: GetLyricFromString
 * 属    性: public & static
 * 功    能: 从行中提取出歌词内容
 * 参    数: const QString  &strTime         要提取的lrc行
 *          QString        &strLyricString  存放提取出的歌词
 * 返    回: LRC_NO_DATA 表示找不到时间标签，成功返回 LRC_SUCCESS
 */
LRC_RESULT QLyric::GetLyricFromString(const QString  &strLine, QString &strLyric)
{
    int   nIndex = 0;

    // 找到最后一个 ']' 的位置
    if ((nIndex = strLine.lastIndexOf("]")) != -1)
    {
        /* 此时,
         * strLine.size() - nIndex - 1
         * 是歌词内容的长度
         */
        strLyric = strLine.right(strLine.size() - nIndex - 1);

        return  LRC_SUCCESS;
    }

    return  LRC_NO_DATA;
}



/*
 * 名    称: AppendLine
 * 属    性: public
 * 功    能: 添加一行歌词
 * 参    数: LYRICLINE  &stLyricLine  要添加的行结构
 */
void  QLyric::AppendLine(const LYRICLINE &stLyricLine)
{
    m_LineList.append(stLyricLine);

    return ;
}



/*
 * 名    称: AppendLine
 * 功    能: 添加一行歌词
 * 属    性: public
 * 参    数: unsigned int   nTime          时间
 *          const QString  &strNewLineData 歌词内容
 */
void  QLyric::AppendLine(LyricTime nTime, const QString  &strNewLine)
{
    LYRICLINE  stLyricLine;

    stLyricLine.nTime       = nTime;
    stLyricLine.strLineData = strNewLine;

    AppendLine(stLyricLine);

    return ;
}



/*
 * 名    称: InsertLine
 * 功    能: 在 nLine 处插入一个新行
 * 属    性: public
 * 参    数: LyricIndex    nLine        行索引
 *          const LYRICLINE    &stLyricLine  要插入的新行
 */
void  QLyric::InsertLine(LyricIndex nLine, const LYRICLINE &stLyricLine)
{
    m_LineList.insert(nLine, stLyricLine);

    return ;
}



/*
 * 名    称: InsertLine
 * 功    能: 在 nLine 处插入一个新行
 * 属    性: public
 * 参    数: LyricIndex    nLine        行索引
 *          unsigned int  nTime        歌词时间
 *          const QString &strNewLineData 歌词内容
 */
void  QLyric::InsertLine(LyricIndex nLine, LyricTime nTime,
                         const QString  &strNewLine)
{
    LYRICLINE   stLyricLine;

    stLyricLine.nTime       = nTime;
    stLyricLine.strLineData = strNewLine;

    InsertLine(nLine, stLyricLine);

    return ;
}



/*
 * 名    称: DeleteLine
 * 功    能: 删除行
 * 属    性: public
 * 参    数: LyricIndex    nLine        要删除行的索引
 * 返    回: 如果找不到索引为 nLine 的数据，返回 LRC_PARA_ERR
 *          调用者可以先确认行索引是否正确
 */
LRC_RESULT QLyric::DeleteLine(LyricIndex nLine)
{
    // 由于 removeAt 不检查参数正确性，
    // 需要进行检验
    if (nLine >= 0 && nLine < m_LineList.size())
    {
        m_LineList.removeAt(nLine);

        return  LRC_SUCCESS;
    }

    return  LRC_PARA_ERR;
}


/*
 * 名    称: AlterLine
 * 功    能: 修改行
 * 属    性: public
 * 参    数: LyricIndex    nLine           要修改行的索引
 *          LYRICLINE     stLyricLineNew  新的行数据
 * 返    回: 如果找不到索引为 nLine 的数据，返回 LRC_PARA_ERR
 *          调用者可以先确认行索引是否正确
 */
LRC_RESULT QLyric::AlterLine(LyricIndex nLine, LYRICLINE stLyricLineNew)
{
    // 检查索引
    if (nLine >= 0 && nLine < m_LineList.size())
    {
        // 去掉行中的换行符
        stLyricLineNew.strLineData.replace("\r", "");
        stLyricLineNew.strLineData.replace("\n", "");

        // 替换
        m_LineList.replace(nLine, stLyricLineNew);

        return  LRC_SUCCESS;
    }
    return  LRC_PARA_ERR;
}



/*
 * 名    称: AlterLine
 * 功    能: 修改行
 * 属    性: public
 * 参    数: LyricIndex    nLine           要修改行的索引
 *          LyricTime     nTimeNew         新的时间
 *          const QString &strNewData      新的歌词内容
 * 返    回: 如果找不到索引为 nLine 的数据，返回 LRC_PARA_ERR
 *          调用者可以先确认行索引是否正确
 */
LRC_RESULT QLyric::AlterLine(LyricIndex nLine, LyricTime nTimeNew,
                             const QString &strNewData)
{
    LYRICLINE  stLyricLineNew;

    stLyricLineNew.nTime       = nTimeNew;
    stLyricLineNew.strLineData = strNewData;

    return  AlterLine(nLine, stLyricLineNew);
}


/*
 * 名    称: GetLyricLine
 * 功    能: 获得索引为 nLine 的行的内容
 * 属    性: public
 * 参    数: LyricIndex    nLine           要获取的行的索引
 *          LYRICLINE     &LyricLine      一个 LYRICLINE 的引用
 * 返    回: 如果找不到索引为 nLine 的数据，返回 LRC_PARA_ERR
 *          调用者可以先确认行索引是否正确
 */
LRC_RESULT QLyric::GetLyricLine(LyricIndex nLine, LYRICLINE &LyricLine)
{
    if (nLine >= 0 && nLine < m_LineList.size())
    {
        LyricLine = m_LineList.at(nLine);

        return  LRC_SUCCESS;
    }
    return  LRC_PARA_ERR;
}


/*
 * 名    称: Sort
 * 功    能: 排序歌词
 * 属    性: public
 */
void  QLyric::Sort()
{
    // 想起第一版写的一个类，还用冒泡排序，感慨啊
    qSort(m_LineList.begin(), m_LineList.end(), &CompareTime);
}


/*
 * 名    称: SetTimeOffset
 * 功    能: 为全部歌词设置时间偏移
 * 属    性: public
 * 参    数: int  nTimeOffsetMS    10 毫秒为单位的时间偏移量
 * 说    明: nTimeOffsetMS 正为后移，负为前移，函数内部会对移动范围进行检查
 */
void QLyric::SetTimeOffset(const int  nTimeOffset10MS)
{
    LYRICLINE  stLine;
    int  i = 0;

    if (nTimeOffset10MS == 0 || m_LineList.empty() != false)
    {
        return ;
    }
    // 以下代码的意思是，如果是向前偏移，则对整个列表进行
    // 排序，确保第一个元素是最小的，并检查前移是否会导致
    // 某些元素出现问题(LyricTime 类型是无符号的)
    if (nTimeOffset10MS < 0)
    {
        Sort();
    }

    foreach (stLine, m_LineList)
    {
        if (nTimeOffset10MS < 0 && m_LineList.at(i).nTime < nTimeOffset10MS)
        {
            stLine.nTime = 0;
        }
        else
        {
            stLine.nTime += nTimeOffset10MS;
        }
        m_LineList.replace(i, stLine);

        i++;
    }
    return ;
}


/*
 * 名    称: GetStringFromTime
 * 功    能: 将以 百分之一秒 为单位的时间数据转换成 [MM:SS:XX] 格式
 *          或 [MM:SS] 的字符串，详见注释
 * 属    性: public
 * 参    数: LyricTime      nTime      以 百分之一秒 为单位的时间
 *          QString       &strTime    转换后的字符串数据
 *          bool           bIsSimple  表示是否以简单形式
 * 返    回: 转换后的字符串
 * 说    明: 见 GetStringFromTime (2)
 */
QString  QLyric::GetStringFromTime(LyricTime nTime, QString  &strTime,
                               bool  bIsSimple /*= false*/)
{
    return  GetStringFromTime(nTime / 6000,       // 分
                              nTime % 6000 / 100, // 秒
                              nTime % 100,        // 百分之一秒
                              strTime,            // 转换后的字符串
                              bIsSimple           // 标志.
                              );
}


/*
 * 名    称: GetStringFromTime
 * 功    能: 将以 百分之一秒 为单位的时间数据转换成 [MM:SS:XX] 格式
 *          或 [MM:SS] 的字符串，详见注释
 * 属    性: public & static
 * 参    数: LyricTime      nMM      以 分 为单位的时间
 *          LyricTime      nSS      以 秒 为单位的时间
 *          LyricTime      nXX      以 百分之一秒 为单位的时间
 *          QString       &strTime    转换后的字符串数据
 *          bool           bIsSimple  表示是否以简单形式
 * 返    回: 转换后的字符串
 * 说    明: 如果 bIsSimple 为 true， 那么当 nXX == 0 时，字符串将转换成
 *          [MM:SS] 形式，不过为了使 LRC 文件更加清晰，建议选false
 */
QString  QLyric::GetStringFromTime(LyricTime nMM, LyricTime nSS,LyricTime nXX,
                                   QString &strTime, bool  bIsSimple /*= false*/)
{
    // 这个函数写的可能有些乱，代码比较多但功能很单一
    // QString 还不是很熟练
    // 使用 QString::sprintf 可能使代码量减少，但是文档不推荐使用
    // 并且不知道跨平台移植会不会出问题（尽管C标准库有sprintf）
    strTime = "[";
    strTime += QString("%1").arg(nMM).rightJustified(2, '0');
    strTime += ":";
    strTime += QString("%1").arg(nSS).rightJustified(2, '0');

    // 如果不要求略写，且 nXX != 0
    // 增加 nXX 段
    if ((!bIsSimple) || (nXX != 0))
    {
        strTime += ".";
        strTime += QString("%1").arg(nXX).rightJustified(2, '0');
    }
    strTime += "]";

    return  strTime;
}



/*
 * 名    称: PrintAll
 * 功    能: 输出 m_LineList 内所有行
 * 属    性: public
 * 说    明: 此函数仅 LRC_DEBUG 声明时可用，供调试使用
 */
#ifdef  LRC_DEBUG

void QLyric::PrintAll()
{
    LYRICLINE  stEachLine;

    qDebug() << "PrintAll()" << endl;
    foreach (stEachLine, m_LineList)
    {
        qDebug() << "  nTime = " << stEachLine.nTime << "strLineData = "
                 << stEachLine.strLineData << endl;
    }
}
#endif



/*
 * 名    称: ResolveLine
 * 功    能: 解析歌词行，并将其添加至列表
 * 属    性: private
 * 参    数: const QString    &strLine   一行歌词数据
 * 返    回: 类型 LRC_RESULT
 * 说    明: 返回 LRC_FORMAT_ERR 表示 strLine 的时间标签格式有错
 *          LRC_NO_DATA 请检查是否含有时间标签， 成功返回 LRC_SUCCESS
 *          如果某一个时间标签无法解析或错误，转换继续执行
 */
LRC_RESULT QLyric::ResolveLine(const QString  &strLine)
{
    // 初始值设置为 true, 这样当 strLine 无时间标签（即应返回
    // LRC_NO_DARA 时）不会导致返回 LRC_FORMAT_ERR
    bool  bErr = false;

    // 检测时间标签准确性
    if (CheckTimeLable(strLine))
    {
        int      nIndex = 0;  // 查找 "[" 的起点
        int      nCutStart = 0,  // 时间标签开始处(自起点之后"["的位置)
                 nCutEnd = 0;    // 时间标签终止处("]")
        QString  strTime;    // 截取到的时间标签
        QString  strLyric;

        // 分解时间标签
        while ((nCutStart = strLine.indexOf("[", nIndex)) != -1)
        {
            nCutEnd = strLine.indexOf("]", nIndex);
            if (nCutStart < nCutEnd)
            {
                strTime = strLine.mid(nCutStart, nCutEnd - nCutStart + 1);
                if (GetLyricFromString(strLine, strLyric) == LRC_SUCCESS)
                {
                    AppendLine(GetTimeFromString(strTime), strLyric);
                }
                else
                {
                    bErr = true;
                }
            }
            nIndex = nCutEnd + 1;
        }

        if (bErr != true)
        {
            return LRC_FORMAT_ERR;
        }
        // 只要有一个标签，nIndex 至少为 1
        // 故 nIndex == 0 返回无标签数据
        if (0 == nIndex)
        {
            return  LRC_NO_DATA;
        }
    }

    return LRC_FORMAT_ERR;
}


/*
 * 名    称: CheckTimeLable
 * 功    能: 检查时间标签是否准确
 * 属    性: private
 * 参    数: QString    strTimeLable  要检查的时间标签
 * 返    回: bool 类型 表示该时间标签是否合法
 * 说    明: 代码有待完善
 */
bool QLyric::CheckTimeLable(const QString  &strTimeLabel)
{
    int     nCount = 0;
    bool    bReturnVal = true;
    QChar   chTimeLabel = '\0';

    if (strTimeLabel.at(0) != QChar('['))
    {
        return false;
    }
    if (strTimeLabel.count('[') != strTimeLabel.count(':'))
    {
        return false;
    }
    foreach(chTimeLabel, strTimeLabel)
    {
        nCount += chTimeLabel == '[' ? 1 : 0;
        nCount -= chTimeLabel == ']' ? 1 : 0;
        if (nCount != 1 && nCount != 0)
        {
            bReturnVal = false;
            break;
        }
    }
    return  bReturnVal;
}




/*
 * 名    称: ResolveLines
 * 功    能: 解析歌词文件
 * 属    性: private
 * 参    数: QStringList   strLines   lrc文件全部歌词行
 * 说    明: 函数会过滤每一行的换行符，并得到歌词相关信息
 */
void  QLyric::ResolveLines(QStringList   strLines)
{
    QString    strLine;

    // 过滤换行符
    strLines.replaceInStrings("\r", "");
    strLines.replaceInStrings("\n", "");

    foreach (strLine, strLines)
    {
        ResolveLine(strLine);
    }

    // 不支持歌词信息标签

}


/*
 * 名    称: LoadFromFile
 * 功    能: 从文件加载歌词
 * 属    性: public
 * 参    数: QString  strFileName     要加载的文件名
 * 返    回: LRC_RESULT 类型
 *          LRC_FILE_ERR 表示无法打开文件或者文件大小为空
 *          返回 LRC_SUCCESS 表示无错误发生
 */
LRC_RESULT QLyric::LoadFromFile(const QString &strFileName)
{
    QFile   LrcFile;
    bool    bApiRet = false;

    LrcFile.setFileName(strFileName);
    bApiRet = LrcFile.open(QIODevice::ReadOnly | QIODevice::Text);

    if (false != bApiRet && 0 != LrcFile.size())
    {
        QString       strLastRead;
        QStringList   Lines;
        QTextStream   LrcInput(&LrcFile);

        // 确保文件指针指向文件头
        LrcInput.seek(0);
        while (false == (strLastRead = LrcInput.readLine()).isNull())
        {
            Lines += strLastRead;

#ifdef  LRC_DEBUG
            qDebug() << strLastRead << endl;
#endif
        }
        LrcFile.close();
        m_LrcFileName = strFileName;

        ResolveLines(Lines);

        return  LRC_SUCCESS;
    }
    return LRC_FILE_ERR;
}


/*
 * 名    称: SaveToFile
 * 功    能: 保存内存中存储的歌词到文件
 * 属    性: public
 * 参    数: const QString     &strFileName    要保存的文件名
 *          bool               IsAscii        是否保存为 ANSI 编码格式
 *                                              false 表示使用 UTF-8 编码
 * 返    回: LRC_RESULT 类型
 *          LRC_FILE_ERR 表示无法保存文件
 *          返回 LRC_SUCCESS 表示无错误发生
 */
LRC_RESULT QLyric::SaveToFile(const QString &strFileName, bool  IsAscii /*=true*/)
{
    bool       bOpenFile = false;
    QFile      LrcFile;
    LYRICLINE  stLine;

    LrcFile.setFileName((strFileName != "") ? strFileName : m_LrcFileName);

    bOpenFile = LrcFile.open(QIODevice::WriteOnly);
    if (false != bOpenFile)
    {
        QString  strTime;
        QString  strLyricLine;

        foreach (stLine, m_LineList)
        {
            strLyricLine = (GetStringFromTime(stLine.nTime, strTime)
                            + stLine.strLineData + "\r\n");
            if (IsAscii)
            {
                LrcFile.write(strLyricLine.toLocal8Bit());
            }
            else
            {
                LrcFile.write(strLyricLine.toUtf8());
            }
        }
        LrcFile.close();

        return  LRC_FILE_ERR;
    }

    return  LRC_SUCCESS;
}


/*
 * 名    称: GetLineCount
 * 属    性: public
 * 功    能: 获得内存中歌词总数(/行)
 * 返    回: unsigned int 类型
 *          歌词行的总数
*/
unsigned int QLyric::GetLineCount()
{
    return  m_LineList.count();
}



/*
 * 名    称: RemoveAll
 * 属    性: public
 * 功    能: 删除所有歌词
*/
void QLyric::RemoveAll()
{
    m_LineList.clear();
}


// FILE_END
